Skip to content

Batch mutate/1 tier-3 write entry point#48

Open
david-w-t wants to merge 7 commits into
davidwt-com:mainfrom
david-w-t:develop
Open

Batch mutate/1 tier-3 write entry point#48
david-w-t wants to merge 7 commits into
davidwt-com:mainfrom
david-w-t:develop

Conversation

@david-w-t

Copy link
Copy Markdown
Contributor

Summary

Adds graphdb_mgr:mutate/1 — the tier-3 batch write entry point sketched
by the write-path transaction-layering seam (the last open follow-up of that
family). It applies an ordered list of add_relationship / retire_node /
unretire_node mutations atomically in one graphdb_mgr:transaction/1:
all commit or none do.

mutate/1 is a plain exported function (not a gen_server:call) — like
transaction/1, it owns the transaction in the caller's process. Three
phases:

  1. static validation — tuple-shape check + the permanent-tier guard;
    no DB, no allocation. Malformed term → {error, {bad_mutation, M}};
    permanent-tier retire/unretire → {error, permanent_node_immutable}.
  2. resource pre-pass (outside the transaction) — resolve the seeded
    attr nrefs once via graphdb_attr:seeded_nrefs/0, allocate one rel-id
    pair per add_relationship via rel_id_server:get_id_pair/0.
  3. one transaction — fold the prepared list in order, dispatching each
    mutation to a tier-1 in-transaction primitive.

The load-bearing invariant: gen_server calls live only in phase 2; the
phase-3 fold calls only the _in_txn primitives and module-local
set_retired_/3, never a gen_server call inside the Mnesia activity.

Contract (opaque, bare-reason)

  • Success: {ok, [ok, …]} — one native value per mutation, list order.
  • Failure: bare {error, Reason} (first aborting mutation), whole batch
    rolled back. No index — keeps mutate/1 drop-in compatible with the
    error handling callers already write for the solo operations.
  • mutate([]) -> {ok, []} (no transaction opened).

The one refactor

To make add_relationship composable inside the batch transaction, its
in-transaction body was extracted verbatim into a new exported tier-1
primitive graphdb_instance:add_relationship_in_txn/9 (the "add, don't
rewrap" pattern from PRs #44/#45). do_add_relationship/7 now allocates the
rel-id pair up-front and delegates — behaviour-identical (the existing
114-case instance suite is the proof).

Scope

Batch covers the three write ops that are fully implemented today.
create_* / delete_node / update_node_avps and symbolic
cross-mutation back-references are deferred (no tier-1 write primitives, or
not-implemented) — see the design doc.

Testing

Full suite green — 465 CT + 105 EUnit = 570 tests, zero failures, zero
compile warnings
. graphdb_mgr_SUITE gains a 10-case mutate group:
empty batch, single add_relationship, single retire/unretire, mixed
all-succeed, atomic rollback, read-your-writes rollback
({endpoint_retired, X}), malformed term, permanent-tier guard, plus
explicit-template and per-direction-AVP forms.

Docs

TASKS.md (IMPLEMENTED), apps/graphdb/CLAUDE.md (both worker blurbs),
docs/Architecture.md (one tier-3 sentence). Design
docs/designs/batch-mutate-design.md; plan
docs/superpowers/plans/2026-06-24-batch-mutate.md.

🤖 Generated with Claude Code

david-w-t and others added 7 commits June 23, 2026 22:06
The last open follow-up of the write-path transaction-layering seam.
mutate([Mutation]) applies a list of add_relationship/retire_node/
unretire_node mutations atomically in one graphdb_mgr:transaction/1,
composing tier-1 primitives. Opaque bare-reason contract ({ok,[ok,...]}
| {error,Reason}) for drop-in compatibility; indexed errors considered
and rejected (contract + mnesia deadlock-restart footgun). One
behaviour-preserving extraction: graphdb_instance:add_relationship_in_txn/9.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF
…ired, X}

Read-your-writes rollback aborts with {endpoint_retired, X} (the reason
graphdb_instance:validate_arc_endpoints_in_txn actually emits), not the
shorthand 'retired'. Also drop test-7's unobservable 'no rel-id allocated'
sub-assertion in favour of the contract (error reason + no rows written).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF
Closes the final-review coverage gap: the 6- and 7-element add_relationship
mutation forms were unexercised through mutate/1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EWukKCbrN8GybaScJGU2kF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant